package model;

import db.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.hibernate.Transaction;

public class Model {

	Transaction transaction;

	public Model() {}

	/**
	 * Adds ainesosa (ingredient) to database.
	 * @param ainesosa to add
	 * @return Ainesosa added to db
	 */
	public Ainesosa addAinesosa(String nimi) {
		Ainesosa a = Ainesosa.searchExactMatch(nimi);

		if (a == null) {
			a = new Ainesosa();
			a.setNimi(nimi);
			transaction =
				DatabaseAccess.getInstance().beginTransaction();
			DatabaseAccess.getInstance().saveOrUpdate(a);
			transaction.commit();
		}

		return a;
	}

	/**
	 * Adds lasi to database.
	 * @param lasin sanallinen kuvaus
	 * @return Lasi added to db
	 */
	public Lasi addLasi(String kuvaus) {
		Lasi l = Lasi.search(kuvaus);

		if (l == null) {
			l = new Lasi();
			l.setKuvaus(kuvaus);
			transaction =
				DatabaseAccess.getInstance().beginTransaction();
			DatabaseAccess.getInstance().saveOrUpdate(l);
			transaction.commit();
		}

		return l;
	}

	/**
	 * Adds menetelmä to database
	 * @param menetelmän kuvaus
	 * @return Menetelmä added to db
	 */
	public Menetelmä addMenetelmä(String kuvaus) {
		Menetelmä m = Menetelmä.search(kuvaus);

		if (m == null) {
			m = new Menetelmä();
			m.setKuvaus(kuvaus);
			transaction =
				DatabaseAccess.getInstance().beginTransaction();
			DatabaseAccess.getInstance().saveOrUpdate(m);
			transaction.commit();
		}

		return m;
	}

	/**
	 * Adds or updates the cocktail.
	 *
	 * @param nimet
	 * @param ainesosat
	 * @param aika
	 * @param lasi
	 * @param lämpötila
	 * @param makeus
	 * @param menetelmä
	 * @param ohje
	 * @return created cocktail.
	 */
	public Drinkki addOrUpdateDrinkki(String[] nimet, String[] ainesosat,
	                          String aika, String lasi, String lämpötila,
	                          String makeus, String menetelmä, String ohje)
				  throws Exception
	{
		Drinkki x = null;

		// prevent users' from doing anything stupid.
		for (String nimi : nimet) {
			Nimi n = Nimi.searchExactMatch(nimi);

			if (n == null)
				continue;

			if (x == null) {
				x = n.getDrinkki();
				continue;
			}

			if (x == n.getDrinkki() || // Alias for same drink: ok.
			    null == n.getDrinkki()) { // New alias: accept.
				continue;
			} else { // Alias refers to ANOTHER drink - a big nono.
				throw new Exception("You fool.");
			}
		}

		Lasi _lasi = addLasi(lasi);
		Ajankohta _aika  = Ajankohta.search(aika);
		Lämpötila _lämpötila = Lämpötila.search(lämpötila);
		Makeus _makeus = Makeus.search(makeus);
		Menetelmä _menetelmä = addMenetelmä(menetelmä);

		if (x == null) {
			// new drink
			x = addDrinkki(
				nimet, ainesosat, _aika, _lasi,
				_lämpötila, _makeus, _menetelmä, ohje
			);
		} else {
			// just update the drink
			x = updateDrinkki(
				nimet, ainesosat, _aika, _lasi,
				_lämpötila, _makeus, _menetelmä, ohje
			);
		}

		return x;
	}

	/**
	 * Adds cocktail to database. Everything must be given for this to
	 * success. No duplicates are made into database.
	 *
	 * @param nimet
	 * @param ainesosat
	 * @param aika
	 * @param lasi
	 * @param lämpötila
	 * @param makeus
	 * @param menetelmä
	 * @param ohje
	 *
	 * @return Drinkki
	 */
	private Drinkki addDrinkki(String[] nimet, String[] ainesosat,
	                     Ajankohta aika, Lasi lasi, Lämpötila lämpötila,
	                     Makeus makeus, Menetelmä menetelmä, String ohje)
	{
		Drinkki x = new Drinkki();

		x.setLasi(lasi);
		x.setLämpötila(lämpötila);
		x.setMakeus(makeus);
		x.setMenetelmä(menetelmä);
		x.setAjankohta(aika);
		x.setOhje(ohje);

		for (String ainesosa : ainesosat) {
			addAinesosa(ainesosa);
		}

		transaction = DatabaseAccess.getInstance().beginTransaction();
		DatabaseAccess.getInstance().save(x);

		// add names.
		for (String nimi : nimet) {
			Nimi n = new Nimi();
			n.setNimi(nimi);
			// TODO: new names must not exceed old names Yleisyys,
			// because otherwise the drink's "tehName" changes"
			// and THAT's really confuzzling...
			n.setYleisyys((int)(Math.random()*100)+1);
			n.setDrinkki(x);
			DatabaseAccess.getInstance().save(n);
		}

		// map ingredients to added cocktail.
		for (String ainesosa : ainesosat) {
			Ainesosa a = Ainesosa.searchExactMatch(ainesosa);

			Drinkki_Ainesosa mapping = new Drinkki_Ainesosa();
			mapping.setAinesosa(a);
			mapping.setDrinkki(x);

			DatabaseAccess.getInstance().save(mapping);
		}

		transaction.commit();

		return x;
	}

	/**
	 * Updates the cocktail given as parameter. The actual operation done
	 * is: cocktail is removed and then cocktail is added.
	 *
	 * @param nimet
	 * @param ainesosat
	 * @param aika
	 * @param lasi
	 * @param lämpötila
	 * @param makeus
	 * @param menetelmä
	 * @param ohje
	 */
	private Drinkki updateDrinkki(String[] nimet, String[] ainesosat,
	                        Ajankohta aika, Lasi lasi, Lämpötila lämpötila,
	                        Makeus makeus, Menetelmä menetelmä, String ohje)
	{
		removeDrinkki(nimet[0]);
		// should flush between these.
		DatabaseAccess.getInstance().flush();
		DatabaseAccess.getInstance().clear();

		return addDrinkki(
			nimet, ainesosat, aika, lasi,
			lämpötila, makeus, menetelmä, ohje
		);
	}

	/**
	 * Remove cocktails from the database by their name. Cascading can't be
	 * used as it would corrupt the database.
	 *
	 * @param name of the cocktail.
	 */
	public void removeDrinkki(String kuvaus) {
		Nimi n = Nimi.searchExactMatch(kuvaus);
		if (n == null) {
			// well.. this shouldn't happen :#
			// TODO: should throw CorruptedDatabase Exception.
			return;
		}

		Drinkki d = n.getDrinkki();
		if (d == null) {
			// this too shouldn't happen
			return;
		}

		Drinkki_Ainesosa.removeByDrinkki(d);

		transaction = DatabaseAccess.getInstance().beginTransaction();
		for (Nimi nimi : d.getNimet()) {
			DatabaseAccess.getInstance().delete(nimi);
		}

		DatabaseAccess.getInstance().delete(d);
		transaction.commit();
	}

	public List<Ainesosa> listAinesosa() {
		return (List<Ainesosa>) new Ainesosa().list();
	}

	public List<Ajankohta> listAjankohta() {
		return (List<Ajankohta>) new Ajankohta().list();
	}

	public List<Lasi> listLasi() {
		return (List<Lasi>) new Lasi().list();
	}

	public List<Lämpötila> listLämpötila() {
		return (List<Lämpötila>) new Lämpötila().list();
	}

	public List<Makeus> listMakeus() {
		return (List<Makeus>) new Makeus().list();
	}

	public List<Menetelmä> listMenetelmä() {
		return (List<Menetelmä>) new Menetelmä().list();
	}

	public List<Nimi> listNimi() {
		return (List<Nimi>) new Nimi().list();
	}

	public List<Drinkki> listDrinkki() {
		return (List<Drinkki>) new Drinkki().list();
	}

	/**
	 * Search function for drinks.
	 * Params are references to database object that drinks should match.
	 * null references are allowed and criterias are ignored for 'em.
	 *
	 * @param nimet matching names from names db.
	 * @param ainesosat that matched the given criteries.
	 * @param ajankohta
	 * @param lasi
	 * @param lämpötila
	 * @param makeus
	 * @param menetelmä
	 * @return list of matching entries
	 */
	public List<Drinkki> search(List<Nimi> nimet, List<Ainesosa> ainesosat,
	                        Ajankohta ajankohta, Lasi lasi,
	                        Lämpötila lämpötila, Makeus makeus,
	                        Menetelmä menetelmä) {

		List<Drinkki> drinks = null;

		if (nimet != null) {
			Set<Drinkki> matchin_drinks = new TreeSet<Drinkki>();
			for (Nimi nimi : nimet) {
				matchin_drinks.add(nimi.getDrinkki());
			}
			drinks = new ArrayList<Drinkki>(matchin_drinks);
		} else {
			drinks = listDrinkki();
		}

		if (ainesosat != null) {
			// traverse all ainesosat for every cocktail.
			// if we can't find connection for aineosa_id and
			// drinkki_id in Drinkki_aineosa table drop it.
			for (int i=drinks.size()-1; i>=0; i--) {
				boolean hasAine = false;

				for (Ainesosa ainesosa : ainesosat) {
					hasAine |= drinks.get(i)
						.hasAinesosa(ainesosa);
				}

				if (hasAine == false) {
					drinks.remove(i);
				}
			}
		}

		if (ajankohta != null) {
			for (int i=drinks.size()-1; i>=0; i--) {
				if (drinks.get(i).getAjankohta() != ajankohta) {
					drinks.remove(i);
				}
			}
		}

		if (lasi != null) {
			for (int i=drinks.size()-1; i>=0; i--) {
				if (drinks.get(i).getLasi() != lasi) {
					drinks.remove(i);
				}
			}
		}

		if (lämpötila != null) {
			for (int i=drinks.size()-1; i>=0; i--) {
				if (drinks.get(i).getLämpötila() != lämpötila) {
					drinks.remove(i);
				}
			}
		}

		if (makeus != null) {
			for (int i=drinks.size()-1; i>=0; i--) {
				if (drinks.get(i).getMakeus() != makeus) {
					drinks.remove(i);
				}
			}
		}

		if (menetelmä != null) {
			for (int i=drinks.size()-1; i>=0; i--) {
				if (drinks.get(i).getMenetelmä() != menetelmä) {
					drinks.remove(i);
				}
			}
		}

		return drinks;
	}

	/**
	 * Remove all data from database that is not part of some drink.
	 */
	public static void removeOrphans() {
		/*
		-- Find orphan Ainesosat.
		DELETE FROM Ainesosat WHERE id NOT IN (SELECT ainesosa_id
			FROM Drinkki_Ainesosa);

		-- Find orphan Ajankohdat.
		DELETE FROM Ajankohdat WHERE id NOT IN (SELECT ajankohta_id
			FROM Drinkit);

		-- Find orphan .
		DELETE FROM Makeudet WHERE id NOT IN (SELECT makeus_id
			FROM Drinkit);

		-- Find orphan .
		DELETE FROM Menetelmät WHERE id NOT IN (SELECT menetelmä_id
			FROM Drinkit);

		-- Find orphan .
		DELETE FROM Lasit WHERE id NOT IN (SELECT lasi_id
			FROM Drinkit);

		-- Find orphan .
		DELETE FROM Lämpötilat WHERE id NOT IN (SELECT lämpötila_id
			FROM Drinkit);

		-- Find orphan Nimet.
		DELETE FROM Nimet WHERE drinkki_id NOT IN (SELECT id
			FROM Drinkit);
		*/
	}

	/**
	 * Validate given plaintext username and password against those found
	 * from database. For nao just use hard coded hashed values for
	 * demonstratinal purposes.
	 *
	 * @param u Username in plaintext String.
	 * @param p Password in plaintext String.
	 */
	public static boolean login(String u, String p) {
		return u.hashCode() == 92668751 && p.hashCode() == 1933289284;
	}
}
